/** * Copyright (c) 2009 - 2012 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package org.candlepin.resteasy; import com.google.common.collect.ImmutableMap; import com.google.inject.Binding; import com.google.inject.Injector; import org.jboss.resteasy.spi.metadata.ResourceBuilder; import org.jboss.resteasy.spi.metadata.ResourceClass; import org.jboss.resteasy.spi.metadata.ResourceLocator; import org.jboss.resteasy.spi.metadata.ResourceMethod; import org.jboss.resteasy.util.GetRestful; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.CookieParam; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; import javax.ws.rs.GET; import javax.ws.rs.HEAD; import javax.ws.rs.HeaderParam; import javax.ws.rs.MatrixParam; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; /** * ResourceLocatorMap holds a mapping of Methods to Resteasy ResourceLocators. This map * is populated during servlet initialization and then locked. The VerifyAuthorizationFilter * then uses the map to find the correct ResourceLocator for a Method so that we can get * the parameters passed to a Method and run authorization checks on them. */ public class ResourceLocatorMap implements Map<Method, ResourceLocator> { private static final Logger log = LoggerFactory.getLogger(ResourceLocatorMap.class); private Map<Method, ResourceLocator> internalMap; private boolean hasBeenInitialized = false; private Injector injector; @Inject public ResourceLocatorMap(Injector injector) { // Maintain the insertion order for nice output in debug statement internalMap = new LinkedHashMap<Method, ResourceLocator>(); this.injector = injector; } @Override public int size() { return internalMap.size(); } @Override public boolean isEmpty() { return internalMap.isEmpty(); } @Override public boolean containsKey(Object key) { return internalMap.containsKey(key); } @Override public boolean containsValue(Object value) { return internalMap.containsKey(value); } @Override public ResourceLocator get(Object key) { return internalMap.get(key); } @Override public ResourceLocator put(Method key, ResourceLocator value) { return internalMap.put(key, value); } @Override public ResourceLocator remove(Object key) { return internalMap.remove(key); } @Override public void putAll(Map<? extends Method, ? extends ResourceLocator> m) { internalMap.putAll(m); } @Override public void clear() { internalMap.clear(); } @Override public Set<Method> keySet() { return internalMap.keySet(); } @Override public Collection<ResourceLocator> values() { return internalMap.values(); } @Override public Set<java.util.Map.Entry<Method, ResourceLocator>> entrySet() { return internalMap.entrySet(); } @SuppressWarnings("rawtypes") public void init() { if (hasBeenInitialized) { throw new IllegalStateException("This map has already been initialized."); } List<Binding<?>> rootResourceBindings = new ArrayList<Binding<?>>(); for (final Binding<?> binding : injector.getBindings().values()) { final Type type = binding.getKey().getTypeLiteral().getType(); if (type instanceof Class) { final Class<?> beanClass = (Class) type; if (GetRestful.isRootResource(beanClass)) { rootResourceBindings.add(binding); } } } for (Binding<?> binding : rootResourceBindings) { Class<?> clazz = (Class) binding.getKey().getTypeLiteral().getType(); if (Proxy.isProxyClass(clazz)) { for (Class<?> intf : clazz.getInterfaces()) { ResourceClass resourceClass = ResourceBuilder.rootResourceFromAnnotations(intf); registerLocators(resourceClass); } } else { ResourceClass resourceClass = ResourceBuilder.rootResourceFromAnnotations(clazz); registerLocators(resourceClass); } } logLocators(); lock(); } /** * Log what resources have been detected and warn about missing media types. * * Not having a @Produces annotation on a method can have subtle effects on the way Resteasy * resolves which method to dispatch a request to. * * Resource resolution order is detailed in the JAX-RS 2.0 specification in section 3.7.2. * Consider the following * * @PUT * @Path("/foo") * public String methodOne() { * ... * } * * @PUT * @Path("/{id}") * @Produces(MediaType.APPLICATION_JSON) * public String methodTwo(@PathParam("id") String id) { * .... * } * * With a cursory reading of the specification, it appears that a request to * * PUT /foo * * should result in methodOne being selected since methodOne's Path has more * literal characters than methodTwo. However, methodTwo has a specific media * type defined and methodOne does not (thus using the wildcard type as a default), * methodTwo is actually the resource selected. * * The same rules apply for @Consumes annotations. */ protected void logLocators() { StringBuffer registered = new StringBuffer("Registered the following RESTful methods:\n"); StringBuffer missingProduces = new StringBuffer(); StringBuffer missingConsumes = new StringBuffer(); for (Method m : keySet()) { String name = m.getDeclaringClass() + "." + m.getName(); registered.append("\t" + name + "\n"); if (!m.isAnnotationPresent(Produces.class)) { missingProduces.append("\t" + name + "\n"); } if (m.isAnnotationPresent(GET.class) || m.isAnnotationPresent(HEAD.class) || m.isAnnotationPresent(DELETE.class) || m.isAnnotationPresent(Deprecated.class)) { /* Technically GET, HEAD, and DELETE are allowed to have bodies (and therefore would * need a Consumes annotation, but the HTTP 1.1 spec states in section 4.3 that any * such body should be ignored. See http://stackoverflow.com/a/983458/6124862 * * Therefore, we won't print warnings on unannotated GET, HEAD, and DELETE methods. * * Deprecated methods are not expected to be updated. */ continue; } if (!m.isAnnotationPresent(Consumes.class)) { missingConsumes.append("\t" + name + "\n"); } else { /* The purpose of all the ridiculousness below is to find methods that * bind objects from the HTTP request body but that are marked as consuming * the star slash star wildcard mediatype. Candlepin only consumes JSON * at the moment but even if that changes we still want to be explicit * about what we accept. */ Consumes consumes = m.getAnnotation(Consumes.class); List<String> mediaTypes = Arrays.asList(consumes.value()); if (mediaTypes.contains(MediaType.WILDCARD)) { Annotation[][] allParamAnnotations = m.getParameterAnnotations(); boolean bindsBody = false; for (Annotation[] paramAnnotations : allParamAnnotations) { boolean boundObjectFromBody = true; for (Annotation a : paramAnnotations) { Class<? extends Annotation> clazz = a.annotationType(); if (QueryParam.class.isAssignableFrom(clazz) || PathParam.class.isAssignableFrom(clazz) || MatrixParam.class.isAssignableFrom(clazz) || HeaderParam.class.isAssignableFrom(clazz) || FormParam.class.isAssignableFrom(clazz) || CookieParam.class.isAssignableFrom(clazz) ) { boundObjectFromBody = false; continue; } } bindsBody = bindsBody || boundObjectFromBody; } if (bindsBody) { log.warn("{} consumes a wildcard media type but binds an object from" + " the request body. Define specific media types consumed instead.", name); } } } } if (log.isDebugEnabled()) { log.trace(registered.toString()); } if (missingProduces.length() != 0) { log.warn("The following methods are missing a Produces annotation:\n{}", missingProduces); } if (missingConsumes.length() != 0) { log.warn("The following methods are missing a Consumes annotation:\n{}", missingConsumes); } } protected void registerLocators(ResourceClass resourceClass) { for (ResourceMethod resourceMethod : resourceClass.getResourceMethods()) { put(resourceMethod.getMethod(), resourceMethod); } for (ResourceLocator resourceMethod : resourceClass.getResourceLocators()) { put(resourceMethod.getMethod(), resourceMethod); } } public synchronized void lock() { hasBeenInitialized = true; internalMap = ImmutableMap.copyOf(internalMap); } }